﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace AutoDb {
	/// <summary>
	/// 
	/// </summary>
	class XmlSourceFileParser : ISourceFileParser {
		private LogContext m_log;

		private Stack<string> m_sourceFiles;

		private System.Xml.Schema.XmlSchemaSet m_schemas;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="log"></param>
		public XmlSourceFileParser(LogContext log) {
			System.Diagnostics.Debug.Assert(log != null);

			m_log = log;
			m_sourceFiles = new Stack<string>();

			m_schemas = new System.Xml.Schema.XmlSchemaSet();
			var xss = GetType()
				.Assembly
				.GetManifestResourceStream("AutoDb.statements.xsd"); // Using embeded schema file.
			using (var xsr = XmlReader.Create(xss)) {
				m_schemas.Add(null, xsr);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileName"></param>
		/// <param name="rootPackage"></param>
		/// <returns></returns>
		IEnumerable<DbModule> ISourceFileParser.Parse(string fileName, string rootPackage) {
			System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(fileName),
				"Argument \"fileName\" cannot be null or empty!");

			string aFileName;
			if (m_sourceFiles.Count != 0) {
				var lastFileName = m_sourceFiles.Peek();
				var lastFileDir = System.IO.Path.GetDirectoryName(lastFileName);
				aFileName = System.IO.Path.Combine(lastFileDir, fileName);
			} else {
				aFileName = fileName;
			}

			aFileName = System.IO.Path.GetFullPath(aFileName);

			if (m_sourceFiles.Contains(aFileName, StringComparer.OrdinalIgnoreCase)) {
				throw new InvalidOperationException(string.Format(Properties.Main.ErrorDuplicatedSourceFile, aFileName));
			} else {
				m_log.Verbose(Properties.Main.ParsingFile, aFileName);
				m_log.Indent();

				m_sourceFiles.Push(aFileName);

				using (var s = new System.IO.FileStream(aFileName, System.IO.FileMode.Open, System.IO.FileAccess.Read)) {
					foreach (var m in ((ISourceFileParser)this).Parse(s, rootPackage)) {
						yield return m;
					}
				} // end of using.

				m_sourceFiles.Pop();

				m_log.Unindent();
				m_log.Verbose(Properties.Main.ParsedFile, aFileName);
			} // end of if.
		} // end of Parse.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="schemas"></param>
		/// <returns></returns>
		private static XmlReaderSettings CreateXmlReaderSettings(System.Xml.Schema.XmlSchemaSet schemas) {
			var settings = new XmlReaderSettings();
			settings.CheckCharacters = true;
			settings.CloseInput = true;
			settings.ConformanceLevel = ConformanceLevel.Document;
			settings.DtdProcessing = DtdProcessing.Ignore;
			settings.ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.AllowXmlAttributes;
			if (schemas != null) {
				settings.ValidationType = ValidationType.Schema;
				settings.Schemas = schemas;
			} else {
				settings.ValidationType = ValidationType.None;
			}

			settings.ValidationEventHandler += new System.Xml.Schema.ValidationEventHandler(SchemaValidationCallback);
			return settings;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private static void SchemaValidationCallback(object sender, System.Xml.Schema.ValidationEventArgs e) {
			if (e.Exception != null)
				throw e.Exception;
			else
				throw new System.Xml.Schema.XmlSchemaException(e.Message);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="li"></param>
		/// <returns></returns>
		private string FormatLineInfo(IXmlLineInfo li) {
			if (li == null) {
				return "?";
			} else {
				return string.Format("{0}({1}:{2})", m_sourceFiles.Peek(), li.LineNumber, li.LinePosition);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <param name="name"></param>
		/// <returns></returns>
		private static string GetString(XElement element, string name) {
			if (element == null) {
				return string.Empty;
			} else {
				var attr = element.Attribute(name);

				if (attr == null) {
					return string.Empty;
				} else {
					return attr.Value;
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static int GetInteger(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			return int.Parse(s);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static bool? GetBoolean(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			s = s.ToLower();
			if (s == "auto") {
				return null;
			} else if (s == "yes" || s == "1" || s == "true" || s == "on") {
				return true;
			} else if (s == "no" || s == "0" || s == "false" || s == "off") {
				return false;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown boolean: \"{0}\".", s);

				return false;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static DbType GetDbType(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			if (s == "string") {
				return DbType.String;
			} else if (s == "integer" || s == "int") {
				return DbType.Integer;
			} else if (s == "long") {
				return DbType.Long;
			} else if (s == "money" || s == "currency" || s == "decimal") {
				return DbType.Decimal;
			} else if (s == "datetime") {
				return DbType.DateTime;
			} else if (s == "date") {
				return DbType.Date;
			} else if (s == "startDate") {
				return DbType.StartDate;
			} else if (s == "startMonth") {
				return DbType.StartMonth;
			} else if (s == "startYear") {
				return DbType.StartYear;
			} else if (s == "endDate") {
				return DbType.EndDate;
			} else if (s == "endMonth") {
				return DbType.EndMonth;
			} else if (s == "endYear") {
				return DbType.EndYear;
			} else if (s == "double") {
				return DbType.Double;
			} else if (s == "bool" || s == "bit" || s == "boolean") {
				return DbType.Boolean;
			} else if (s == "binary") {
				return DbType.Binary;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown db type \"{0}\".", s);

				return DbType.String;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static ResultType GetResultType(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			if (s == "table") {
				return ResultType.Table;
			} else if (s == "table-single-row") {
				return ResultType.TableSingleRow;
			} else if (s == "string") {
				return ResultType.String;
			} else if (s == "int" || s == "integer") {
				return ResultType.Integer;
			} else if (s == "short") {
				return ResultType.Short;
			} else if (s == "long") {
				return ResultType.Long;
			} else if (s == "double") {
				return ResultType.Double;
			} else if (s == "datetime" || s == "date") {
				return ResultType.DateTime;
			} else if (s == "money" || s == "currency" || s == "decimal") {
				return ResultType.Decimal;
			} else if (s == "bit" || s == "bool" || s == "boolean") {
				return ResultType.Boolean;
			} else if (s == "binary") {
				return ResultType.Binary;
			} else if (s == "list-string") {
				return ResultType.StringList;
			} else if (s == "list-int" || s == "list-integer") {
				return ResultType.IntegerList;
			} else if (s == "list-short") {
				return ResultType.ShortList;
			} else if (s == "list-long") {
				return ResultType.LongList;
			} else if (s == "list-double") {
				return ResultType.DoubleList;
			} else if (s == "list-datetime" || s == "list-date") {
				return ResultType.DateTimeList;
			} else if (s == "list-money" || s == "list-currency" || s == "list-decimal") {
				return ResultType.DecimalList;
			} else if (s == "list-bit" || s == "list-bool" || s == "list-boolean") {
				return ResultType.BooleanList;
			} else if (s == "list-binary") {
				return ResultType.BinaryList;
			} else if (s == "updated-count") {
				return ResultType.UpdatedRows;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown result type \"{0}\".", s);

				return ResultType.UpdatedRows;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static MatchType GetMatchType(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			if (s == "exact") {
				return MatchType.Exact;
			} else if (s == "ends-with") {
				return MatchType.EndsWith;
			} else if (s == "starts-with") {
				return MatchType.StartsWith;
			} else if (s == "contains") {
				return MatchType.Contains;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown match type \"{0}\".", s);

				return MatchType.Exact;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static Direction GetDirection(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			if (s == "input") {
				return Direction.Input;
			} else if (s == "output") {
				return Direction.Output;
			} else if (s == "inputOutput") {
				return Direction.InputOutput;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown direction \"{0}\".", s);

				return Direction.Input;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static DynamicTextType GetDynamicTextType(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			if (s == "exists") {
				return DynamicTextType.Exists;
			} else if (s == "notExists") {
				return DynamicTextType.NotExists;
			} else if (s == "isNull") {
				return DynamicTextType.IsNull;
			} else if (s == "isNotNull") {
				return DynamicTextType.IsNotNull;
			} else if (s == "isEmpty") {
				return DynamicTextType.IsEmpty;
			} else if (s == "isNotEmpty") {
				return DynamicTextType.IsNotEmpty;
			} else if (s == "isNullOrEmpty") {
				return DynamicTextType.IsNullOrEmpty;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown dynamic text type \"{0}\".", s);

				return DynamicTextType.Exists;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="s"></param>
		/// <returns></returns>
		private static CommandType GetCommandType(string s) {
			System.Diagnostics.Debug.Assert(s != null);

			if (s == "text") {
				return CommandType.Text;
			} else if (s == "sp" || s == "storeprocedure") {
				return CommandType.StoredProcedure;
			} else {
				System.Diagnostics.Debug.Assert(false, "", "Unknown CommandType \"{0}\".", s);

				return CommandType.Text;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="stream"></param>
		/// <returns></returns>
		private XElement LoadXElement(System.IO.Stream stream) {
			var settings = CreateXmlReaderSettings(m_schemas);

			using (var xr = XmlReader.Create(stream, settings)) {
				try {
					return XElement.Load(xr, LoadOptions.SetLineInfo);
				} catch (System.Xml.Schema.XmlSchemaException se) {
					var psi = string.Format(Properties.Main.ErrorValidatingSchema,
						m_sourceFiles.Peek(),
						se.LineNumber,
						se.LinePosition);

					throw new InvalidOperationException(psi, se);
				}
			} // end of using.
		} // end of LoadXElement.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="stream"></param>
		/// <param name="rootPackage"></param>
		/// <returns></returns>
		IEnumerable<DbModule> ISourceFileParser.Parse(System.IO.Stream stream, string rootPackage) {
			var doc = LoadXElement(stream);

			System.Diagnostics.Debug.Assert(doc != null);

			foreach (var child in doc.Elements()) {
				if (child.Name == "module") {
					yield return ParseModule(child, rootPackage);
				} else if (child.Name == "include") {
					foreach (var m in ParseInclude(child, rootPackage)) {
						yield return m;
					}
				} else {
					throw new InvalidOperationException(
						string.Format(Properties.Main.ErrorUnknownElement,
						child.Name,
						FormatLineInfo(child as IXmlLineInfo)));
				}
			} // end of foreach.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <param name="rootPackage"></param>
		/// <returns></returns>
		IEnumerable<DbModule> ParseInclude(XElement element, string rootPackage) {
			System.Diagnostics.Debug.Assert(element.Name == "include");

			var src = GetString(element, "src");

			return ((ISourceFileParser)this).Parse(src, rootPackage);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <param name="rootPackage"></param>
		/// <returns></returns>
		DbModule ParseModule(XElement element, string rootPackage) {
			System.Diagnostics.Debug.Assert(element.Name == "module");

			var name = GetString(element, "name");
			var package = GetString(element, "package");
			var description = GetString(element, "description");

			m_log.Verbose("Parsing module \"{0}\"...", name);
			m_log.Indent();

			var m = new DbModule(FormatLineInfo(element as IXmlLineInfo));

			m.Name = name;
			m.Package = DbModule.MakeFullPackage(rootPackage, package);
			m.Description = description;

			foreach (var child in element.XPathSelectElements("table")) {
				m.Tables.Add(ParseTable(child));
			}

			foreach (var child in element.XPathSelectElements("stmt|statement")) {
				m.Statements.Add(ParseStatement(child));
			}

			m_log.Unindent();
			m_log.Verbose("Parsed module \"{0}\".", name);

			return m;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <returns></returns>
		DbTable ParseTable(XElement element) {
			var name = GetString(element, "name");

			m_log.Verbose(Properties.Main.ParsingModule, name);
			m_log.Indent();

			var t = new DbTable(FormatLineInfo(element));

			t.Name = name;

			foreach (var child in element.XPathSelectElements("column")) {
				t.Add(ParseColumn(child));
			}

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsedModule, name);

			return t;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <returns></returns>
		DbColumn ParseColumn(XElement element) {
			var name = GetString(element, "name");

			m_log.Verbose(Properties.Main.ParsingColumn, name);
			m_log.Indent();

			var c = new DbColumn(FormatLineInfo(element));

			c.Name = name;
			c.DbType = GetDbType(GetString(element, "type"));
			c.Size = GetInteger(GetString(element, "size"));
			c.Description = GetString(element, "description");
			c.Unique = GetBoolean(GetString(element, "unique"));
			c.PrimaryKey = GetBoolean(GetString(element, "primaryKey"));

			var atd = element.Attribute("defaultValue");
			if (atd == null) {
				c.DefaultValue = null;
			} else {
				c.DefaultValue = atd.Value;
			}

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsedColumn, name);

			return c;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <returns></returns>
		DbStatement ParseStatement(XElement element) {
			var name = GetString(element, "name");

			m_log.Verbose(Properties.Main.ParsingStatement, name);
			m_log.Indent();

			var s = new DbStatement(FormatLineInfo(element));

			s.Name = name;
			s.AllowPaging = GetBoolean(GetString(element, "allowPaging"));
			s.ReadOnly = GetBoolean(GetString(element, "readOnly"));
			s.ResultType = GetResultType(GetString(element, "resultType"));
			s.ResultTable = GetString(element, "resultTable");
			s.Description = GetString(element, "description");
			s.CommandType = GetCommandType(GetString(element, "commandType"));

			var dc = 0;
			var sc = 0;
			foreach (var child in element.Nodes()) {
				if (child is XText) {
					var tchild = (XText)child;
					if (tchild.Value.Trim().Length == 0) {
						continue;
					}

					s.Texts.Add(ParseStaticText(tchild, sc++));
				}

				if (child is XElement) {
					var echild = (XElement)child;
					if (echild.Name == "dynamic") {
						s.Texts.Add(ParseDynamicText(echild, dc++));
					} else if (echild.Name == "parameter") {
						s.Parameters.Add(ParseParameter(echild));
					}
				}
			}

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsedStatement, name);

			return s;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="text"></param>
		/// <param name="c"></param>
		/// <returns></returns>
		DbStaticText ParseStaticText(XText text, int c) {
			m_log.Verbose(Properties.Main.ParsingStatic, c);
			m_log.Indent();

			var s = new DbStaticText(FormatLineInfo(text));
			s.Text = text.Value.Trim();

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsedStatic, c);

			return s;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <param name="c"></param>
		/// <returns></returns>
		DbDynamicText ParseDynamicText(XElement element, int c) {
			m_log.Verbose(Properties.Main.ParsingDynamic, c);
			m_log.Indent();

			var d = new DbDynamicText(FormatLineInfo(element));

			d.Prepend = GetString(element, "prepend");

			int tc = 0;
			foreach (var child in element.XPathSelectElements("*")) {
				d.Texts.Add(ParseDynamicTextItem(child, tc++));
			}

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsedDynamic, c);

			return d;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <param name="c"></param>
		/// <returns></returns>
		DbDynamicTextItem ParseDynamicTextItem(XElement element, int c) {
			m_log.Verbose(Properties.Main.ParsingDynamicText, c);
			m_log.Indent();

			var t = new DbDynamicTextItem(FormatLineInfo(element));

			t.DynamicTextType = GetDynamicTextType(element.Name.LocalName);
			t.Parameter = GetString(element, "parameter");
			t.Text = element.Value.Trim();

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsingDynamicText, c);

			return t;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="element"></param>
		/// <returns></returns>
		DbParameter ParseParameter(XElement element) {
			var name = GetString(element, "name");

			m_log.Verbose(Properties.Main.ParsingParameter, name);
			m_log.Indent();

			var p = new DbParameter(FormatLineInfo(element));

			p.Name = name;
			p.DbType = GetDbType(GetString(element, "type"));
			p.MatchType = GetMatchType(GetString(element, "matchType"));
			p.Direction = GetDirection(GetString(element, "direction"));
			p.Size = GetInteger(GetString(element, "size"));
			p.Description = GetString(element, "description");

			m_log.Unindent();
			m_log.Verbose(Properties.Main.ParsedParameter, name);

			return p;
		}
	} // end of XmlSourceFileParser.
}
